""" S3D.ElectricCraft.ElectricItems module """

from S3D.ElectricCraft import ViewModel as game
from S3D import range_2
from collections import deque
from S3D.ElectricCraft.ViewModel import ViewGroup

ELECTRIC_ITEM_INPUT         = 0
ELECTRIC_ITEM_OUTPUT        = 1
ELECTRIC_ITEM_DIODE         = 2
ELECTRIC_ITEM_KEY           = 3
ELECTRIC_ITEM_POWER         = 4
ELECTRIC_ITEM_CHIP          = 5

ELECTRIC_ITEM_NAMES         = lambda: [name for name in globals() if name.startswith("ELECTRIC_ITEM_")]
ELECTRIC_ITEM_NAMES         = ELECTRIC_ITEM_NAMES()
ELECTRIC_ITEM_NAMES.sort(key = lambda x: globals()[x])

class ElectricItem:
    def __init__(self, type, group):
        self.type       = type
        self.group      = group
        self.inputs     = []        # list of input groups
        self.outputs    = []        # list of output groups
        self.state      = []        # current output states

    def start(self, model):
        pass

    def update(self, model):
        pass

    def powerInput(self, model, index):
        pass

    def initAllNeighbours(self, x, y, viewModel, isOutput):
        # all neighbours are outputs
        for dir in range(game.DIRS_COUNT):
            group = viewModel.getNeighbourWireGroup(x, y, dir)
            if group is not None:
                if isOutput:
                    self.outputs.append(group)
                else:
                    self.inputs.append(group)

        self.initOutputStates()

    def initOutputStates(self):
        for index in range(len(self.outputs)):
            self.state.append(0)

class ElecInputItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        item = viewModel.items[x][y]
        super(ElecInputItem, self).__init__(ELECTRIC_ITEM_INPUT, item.group)
        self.initAllNeighbours(x, y, viewModel, True)
        self.index = item.index
        self.userState = False

    def isActive(self):
        return self.userState

    def setUserState(self, model, state):
        self.userState = state
        model.needUpdateWires = True

class ElecOutputItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        item = viewModel.items[x][y]
        super(ElecOutputItem, self).__init__(ELECTRIC_ITEM_OUTPUT, item.group)
        self.initAllNeighbours(x, y, viewModel, False)
        self.index = item.index

    def reset(self):
        self.userState = False

    def powerInput(self, model, index):
        self.userState = True

    def getUserState(self):
        return self.userState

class ElecPowerItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        super(ElecPowerItem, self).__init__(ELECTRIC_ITEM_POWER, viewModel.items[x][y].group)
        self.initAllNeighbours(x, y, viewModel, True)

    def isActive(self):
        return True

class ElecDiodeItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        item = viewModel.items[x][y]
        super(ElecDiodeItem, self).__init__(ELECTRIC_ITEM_DIODE, item.group)

        group = viewModel.getNeighbourWireGroup(x, y, item.direction)
        if group is not None:
            self.outputs.append(group)

        invDir = game.getRevertedDir(item.direction)
        group = viewModel.getNeighbourWireGroup(x, y, invDir)
        if group is not None:
            self.inputs.append(group)

        self.initOutputStates()

    def powerInput(self, model, index):
        for group in self.outputs:
            model.powerGroup(group)

class ElecKeyItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        item = viewModel.items[x][y]

        super(ElecKeyItem, self).__init__(ELECTRIC_ITEM_KEY, item.group)

        self.group              = item.group
        self.defaultOpenState   = item.state
        self.openState          = item.state
        self.nextOpenState      = item.state

        # direct neighbours are both input and outputs
        for dir in (item.direction, game.getRevertedDir(item.direction)):
            group = viewModel.getNeighbourWireGroup(x, y, dir)
            # append None values too
            self.inputs.append(group)
            self.outputs.append(group)

        # key control inputs
        rotDir = game.getRotatedDir(item.direction)
        for dir in (rotDir, game.getRevertedDir(rotDir)):
            group = viewModel.getNeighbourWireGroup(x, y, dir)
            # append None values too
            self.inputs.append(group)

        self.initOutputStates()

    def reset(self):
        self.nextOpenState = self.defaultOpenState

    def powerInput(self, model, index):
        if index < 2: # transpass signal
            if self.openState:
                model.powerGroup(self.outputs[(index + 1) % 2])
        else:
            self.nextOpenState = not self.defaultOpenState

    def update(self, model):
        cur, next = self.openState, self.nextOpenState
        if cur == next:
            return

        self.openState = next
        model.notifyKeySwitch(self.group, next != self.defaultOpenState)

class ElecChipItem(ElectricItem):
    def __init__(self, x, y, viewModel):
        item = viewModel.items[x][y]
        super(ElecChipItem, self).__init__(ELECTRIC_ITEM_CHIP, item.group)

        chipModel = item.chip.model
        maxInputs = chipModel.maxInputs

        # get inputs connections from left and outputs from right
        for index in range(maxInputs):
            self.inputs.append(viewModel.getNeighbourWireGroup(x,      y + index, 0))
            self.outputs.append(viewModel.getNeighbourWireGroup(x + 1, y + index, 2))

        self.model = buildElectricModel(chipModel)

        self.lastState  = [False for _ in range(maxInputs)]
        self.curState   = [False for _ in range(maxInputs)]

    def update(self, model):
        self.model.updateItems()

        # reset current inputs state
        for index in range(len(self.curState)):
            self.curState[index] = False

        if self.model.needUpdateWires:
            model.needUpdateWires = True

    def powerInput(self, model, index):
        self.curState[index] = True

    def updateModel(self, model):
        for index, val in enumerate(self.curState):
            if self.lastState[index] != val:
                self.lastState[index] = val
                self.model.needUpdateWires = True

            p = self.model.inputs[index]
            if p is not None:
                p.setUserState(self.model, val)

        self.model.update(False)

        # retrieve data from outputs
        for index, p in enumerate(self.model.outputs):
            if p and p.getUserState():
                model.powerGroup(self.outputs[p.index])

class ElectricModel:
    def __init__(self, viewModel):
        self.viewModel = viewModel
        self.groups = viewModel.groups # share same groups
        self.items  = []
        self.groupInputs = [[] for _ in self.groups]
        self.groupsToUpdate = deque([])

        self.inputs         = [None for _ in range(viewModel.maxInputs)]
        self.outputs        = [None for _ in range(viewModel.maxInputs)]
        self.powerSources   = []
        self.wireGroups     = []
        self.chips          = []
        self.keys           = []

        self.needUpdateWires = True

    def getGroupsCount(self):
        res = len(self.groups)

        for p in self.chips:
            res += p.model.getGroupsCount()

        return res

    def powerGroup(self, group):
        if group is None or self.groups[group].state:
            return

        self.groups[group].state = True
        self.groupsToUpdate.append(group)

    def notifyKeySwitch(self, group, switch):
        self.groups[group].switch = switch
        self.needUpdateWires = True

    def setInputState(self, index, state):
        item = self.inputs[index]
        item.setUserState(self, state)

    def updateItems(self):
        for p in self.items:
            p.update(self)
        
    def update(self, updateItems = True):
        # update items
        if updateItems:
            self.updateItems()

        if not self.needUpdateWires:
            return

        # reset wire groups & keys
        for group in self.wireGroups:
            self.groups[group].state = False

        for p in self.keys:
            p.reset()

        for p in self.outputs:
            if p is not None:
                p.reset()

        # initial power sources
        for p in self.powerSources:
            if not p.isActive():
                continue

            for group in p.outputs:
                self.powerGroup(group)

        # update wires
        if len(self.groupsToUpdate) == 0:
            for chip in self.chips:
                chip.updateModel(self)
            
        while len(self.groupsToUpdate) > 0:
            while len(self.groupsToUpdate) > 0:
                group = self.groupsToUpdate.popleft()

                for item, inputIndex in self.groupInputs[group]:
                    item.powerInput(self, inputIndex)

            for chip in self.chips:
                chip.updateModel(self)

        self.needUpdateWires = False                
        self.viewModel.update()

def buildElectricModel(viewModel):
    if viewModel is None:
        return ElectricModel(viewModel)

    sizeX, sizeY = viewModel.sizeX, viewModel.sizeY

    elements = []
    wireGroups = []

    # reset groups
    groups = viewModel.groups = []
    for x, y in range_2(sizeX, sizeY):
        item = viewModel.items[x][y]
        if item is None:
            continue
        item.group = None if item.type != game.VIEW_ITEM_BRIDGE else [None, None]

    # collect groups
    for x, y in range_2(sizeX, sizeY):
        item = viewModel.items[x][y]
        if item is None:
            continue
        if item.type != game.VIEW_ITEM_BRIDGE and item.group is not None:
            continue

        if item.type == game.VIEW_ITEM_WIRE or item.type == game.VIEW_ITEM_BRIDGE:
            markWireGroup(x, y, viewModel, wireGroups)
        else:
            item.group = len(groups)
            groups.append(ViewGroup())
            elements.append((x, y))

    model = ElectricModel(viewModel)
    model.wireGroups = wireGroups
    model.chips = []

    for x, y in elements:
        item = viewModel.items[x][y]
        if item.type == game.VIEW_ITEM_INPUT:
            model.items.append(ElecInputItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_OUTPUT:
            model.items.append(ElecOutputItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_POWER:
            model.items.append(ElecPowerItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_DIODE:
            model.items.append(ElecDiodeItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_KEY:
            model.items.append(ElecKeyItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_CHIP_INPUT and item.index == 0: # chip top-left corner
            model.items.append(ElecChipItem(x, y, viewModel))
        elif item.type == game.VIEW_ITEM_MARK or item.type in game.CHIP_PARTS:
            pass
        else:
            print("Unknown element: %s" % game.VIEW_ITEM_NAMES[item.type])

    # collect group inputs
    for p in model.items:
        if p.type == ELECTRIC_ITEM_INPUT:
            model.inputs[p.index] = p
            model.powerSources.append(p)
        elif p.type == ELECTRIC_ITEM_OUTPUT:
            model.outputs[p.index] = p # callback list
        elif p.type == ELECTRIC_ITEM_POWER:
            model.powerSources.append(p)
        elif p.type == ELECTRIC_ITEM_KEY:
            model.keys.append(p)
        elif p.type == ELECTRIC_ITEM_CHIP:
            model.chips.append(p)

        for index, group in enumerate(p.inputs):
            if group is not None:
                model.groupInputs[group].append((p, index))

    return model

HORIZONTAL_CONSTR = (False, True, False, True)
VERTICAL_CONSTR   = (True, False, True, False)

def markWireGroup(x, y, model, wireGroups, dir = None):
    items = model.items
    sizeX, sizeY = model.sizeX, model.sizeY

    first = items[x][y]
    if first.type == game.VIEW_ITEM_BRIDGE and dir is None:
        if first.group[0] is None:
            markWireGroup(x, y, model, wireGroups, 0)
        if first.group[1] is None:
            markWireGroup(x, y, model, wireGroups, 1)
        return

    if first.type == game.VIEW_ITEM_WIRE and first.group is not None:
        return

    groupIndex = len(model.groups)
    model.groups.append(ViewGroup())
    wireGroups.append(groupIndex)

    def canMove(cur, prevDir, x, y, dir):
        # current item
        if cur.type != game.VIEW_ITEM_BRIDGE:
            constr = cur.constr
        elif prevDir == 0: # bridge wire direction
            constr = HORIZONTAL_CONSTR
        else:
            constr = VERTICAL_CONSTR

        if constr[dir]:
            return False

        if x < 0 or x >= sizeX or y < 0 or y >= sizeY:
            return False

        # neighbour item
        invDir = game.getRevertedDir(dir)
        item = items[x][y]
        if item is None or item.constr[invDir]:
            return False

        if item.type != game.VIEW_ITEM_WIRE and item.type != game.VIEW_ITEM_BRIDGE:
            return False
        
        if item.type == game.VIEW_ITEM_WIRE and item.group is not None:
            return False

        if item.type == game.VIEW_ITEM_BRIDGE:
            if item.group[dir % 2] is not None:
                return False

        return True

    queue = deque([(x, y, dir)])
    while len(queue) > 0:
        x, y, prevDir = queue.popleft()
        cur  = items[x][y]
        if cur.type != game.VIEW_ITEM_BRIDGE:
            cur.group = groupIndex
        else:
            cur.group[prevDir] = groupIndex

        for dir, (dx, dy) in enumerate(game.DIR_LIST):
            nextX, nextY = x + dx, y + dy
            if not canMove(cur, prevDir, nextX, nextY, dir):
                continue

            queue.append((nextX, nextY, dir % 2)) # only two directions: horizontal and vertical